/*
* eID Applet Project.
* Copyright (C) 2008-2010 FedICT.
* Copyright (C) 2014-2015 e-Contract.be BVBA.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, see
* http://www.gnu.org/licenses/.
*/
package test.unit.be.fedict.eid.applet.service.signer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
import javax.crypto.Cipher;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xml.security.utils.Constants;
import org.apache.xpath.XPathAPI;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.ocsp.OCSPResp;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.joda.time.DateTime;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import be.fedict.eid.applet.service.signer.AbstractXmlSignatureService;
import be.fedict.eid.applet.service.signer.DigestAlgo;
import be.fedict.eid.applet.service.signer.SignatureFacet;
import be.fedict.eid.applet.service.signer.TemporaryDataStorage;
import be.fedict.eid.applet.service.signer.facets.EnvelopedSignatureFacet;
import be.fedict.eid.applet.service.signer.facets.ExplicitSignaturePolicyService;
import be.fedict.eid.applet.service.signer.facets.KeyInfoSignatureFacet;
import be.fedict.eid.applet.service.signer.facets.RevocationData;
import be.fedict.eid.applet.service.signer.facets.RevocationDataService;
import be.fedict.eid.applet.service.signer.facets.SignaturePolicyService;
import be.fedict.eid.applet.service.signer.facets.XAdESSignatureFacet;
import be.fedict.eid.applet.service.signer.facets.XAdESXLSignatureFacet;
import be.fedict.eid.applet.service.signer.ooxml.Office2010SignatureFacet;
import be.fedict.eid.applet.service.signer.time.TimeStampService;
import be.fedict.eid.applet.service.spi.DigestInfo;
public class XAdESSignatureFacetTest {
private static final Log LOG = LogFactory.getLog(XAdESSignatureFacetTest.class);
private static class XmlSignatureTestService extends AbstractXmlSignatureService {
private TemporaryTestDataStorage temporaryDataStorage;
private ByteArrayOutputStream signedDocumentOutputStream;
public XmlSignatureTestService(SignatureFacet... signatureFacets) {
super(DigestAlgo.SHA1);
this.temporaryDataStorage = new TemporaryTestDataStorage();
this.signedDocumentOutputStream = new ByteArrayOutputStream();
for (SignatureFacet signatureFacet : signatureFacets) {
addSignatureFacet(signatureFacet);
}
setSignatureNamespacePrefix("ds");
}
public byte[] getSignedDocumentData() {
return this.signedDocumentOutputStream.toByteArray();
}
@Override
protected OutputStream getSignedDocumentOutputStream() {
return this.signedDocumentOutputStream;
}
@Override
protected TemporaryDataStorage getTemporaryDataStorage() {
return this.temporaryDataStorage;
}
public String getFilesDigestAlgorithm() {
return null;
}
}
@BeforeClass
public static void beforeClass() {
if (null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void testSignEnvelopingDocument() throws Exception {
// setup
EnvelopedSignatureFacet envelopedSignatureFacet = new EnvelopedSignatureFacet();
KeyInfoSignatureFacet keyInfoSignatureFacet = new KeyInfoSignatureFacet(true, false, false);
SignaturePolicyService signaturePolicyService = null;
// SignaturePolicyService signaturePolicyService = new
// ExplicitSignaturePolicyService(
// "urn:test", "hello world".getBytes(), "description",
// "http://here.com");
XAdESSignatureFacet xadesSignatureFacet = new XAdESSignatureFacet(signaturePolicyService);
TimeStampService mockTimeStampService = EasyMock.createMock(TimeStampService.class);
RevocationDataService mockRevocationDataService = EasyMock.createMock(RevocationDataService.class);
XAdESXLSignatureFacet xadesXLSignatureFacet = new XAdESXLSignatureFacet(mockTimeStampService,
mockRevocationDataService);
XmlSignatureTestService testedInstance = new XmlSignatureTestService(envelopedSignatureFacet,
keyInfoSignatureFacet, xadesSignatureFacet, xadesXLSignatureFacet);
KeyPair keyPair = PkiTestUtils.generateKeyPair();
DateTime notBefore = new DateTime();
DateTime notAfter = notBefore.plusYears(1);
X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), "CN=Test", notBefore,
notAfter, null, keyPair.getPrivate(), true, 0, null, null, new KeyUsage(KeyUsage.nonRepudiation));
List<X509Certificate> certificateChain = new LinkedList<X509Certificate>();
/*
* We need at least 2 certificates for the XAdES-C complete certificate
* refs construction.
*/
certificateChain.add(certificate);
certificateChain.add(certificate);
RevocationData revocationData = new RevocationData();
final X509CRL crl = PkiTestUtils.generateCrl(certificate, keyPair.getPrivate());
revocationData.addCRL(crl);
OCSPResp ocspResp = PkiTestUtils.createOcspResp(certificate, false, certificate, certificate,
keyPair.getPrivate(), "SHA1withRSA");
revocationData.addOCSP(ocspResp.getEncoded());
// expectations
EasyMock.expect(mockTimeStampService.timeStamp(EasyMock.anyObject(byte[].class),
EasyMock.anyObject(RevocationData.class))).andStubAnswer(new IAnswer<byte[]>() {
public byte[] answer() throws Throwable {
Object[] arguments = EasyMock.getCurrentArguments();
RevocationData revocationData = (RevocationData) arguments[1];
revocationData.addCRL(crl);
return "time-stamp-token".getBytes();
}
});
EasyMock.expect(mockRevocationDataService.getRevocationData(EasyMock.eq(certificateChain)))
.andStubReturn(revocationData);
// prepare
EasyMock.replay(mockTimeStampService, mockRevocationDataService);
// operate
DigestInfo digestInfo = testedInstance.preSign(null, certificateChain, null, null, null);
// verify
assertNotNull(digestInfo);
assertEquals("SHA-1", digestInfo.digestAlgo);
assertNotNull(digestInfo.digestValue);
TemporaryTestDataStorage temporaryDataStorage = (TemporaryTestDataStorage) testedInstance
.getTemporaryDataStorage();
assertNotNull(temporaryDataStorage);
InputStream tempInputStream = temporaryDataStorage.getTempInputStream();
assertNotNull(tempInputStream);
Document tmpDocument = PkiTestUtils.loadDocument(tempInputStream);
LOG.debug("tmp document: " + PkiTestUtils.toString(tmpDocument));
Element nsElement = tmpDocument.createElement("ns");
nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS);
nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:xades", "http://uri.etsi.org/01903/v1.3.2#");
Node digestValueNode = XPathAPI.selectSingleNode(tmpDocument, "//ds:DigestValue", nsElement);
assertNotNull(digestValueNode);
String digestValueTextContent = digestValueNode.getTextContent();
LOG.debug("digest value text content: " + digestValueTextContent);
assertFalse(digestValueTextContent.isEmpty());
/*
* Sign the received XML signature digest value.
*/
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue);
byte[] signatureValue = cipher.doFinal(digestInfoValue);
/*
* Operate: postSign
*/
testedInstance.postSign(signatureValue, certificateChain);
// verify
EasyMock.verify(mockTimeStampService, mockRevocationDataService);
byte[] signedDocumentData = testedInstance.getSignedDocumentData();
assertNotNull(signedDocumentData);
Document signedDocument = PkiTestUtils.loadDocument(new ByteArrayInputStream(signedDocumentData));
LOG.debug("signed document: " + PkiTestUtils.toString(signedDocument));
NodeList signatureNodeList = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
assertEquals(1, signatureNodeList.getLength());
Element signatureNode = (Element) signatureNodeList.item(0);
// work-around for Java 7
Element signedPropertiesElement = (Element) signatureNode
.getElementsByTagNameNS(XAdESXLSignatureFacet.XADES_NAMESPACE, "SignedProperties").item(0);
signedPropertiesElement.setIdAttribute("Id", true);
DOMValidateContext domValidateContext = new DOMValidateContext(
KeySelector.singletonKeySelector(keyPair.getPublic()), signatureNode);
XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance();
XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext);
boolean validity = xmlSignature.validate(domValidateContext);
assertTrue(validity);
File tmpFile = File.createTempFile("xades-x-l-", ".xml");
FileUtils.writeStringToFile(tmpFile, PkiTestUtils.toString(signedDocument));
LOG.debug("tmp file: " + tmpFile.getAbsolutePath());
Node resultNode = XPathAPI.selectSingleNode(signedDocument,
"ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties/xades:SignedSignatureProperties/xades:SigningCertificate/xades:Cert/xades:CertDigest/ds:DigestValue",
nsElement);
assertNotNull(resultNode);
// also test whether the XAdES extension is in line with the XAdES XML
// Schema.
// stax-api 1.0.1 prevents us from using
// "XMLConstants.W3C_XML_SCHEMA_NS_URI"
Node qualifyingPropertiesNode = XPathAPI.selectSingleNode(signedDocument,
"ds:Signature/ds:Object/xades:QualifyingProperties", nsElement);
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
LSResourceResolver xadesResourceResolver = new XAdESLSResourceResolver();
factory.setResourceResolver(xadesResourceResolver);
InputStream schemaInputStream = XAdESSignatureFacetTest.class.getResourceAsStream("/XAdESv141.xsd");
Source schemaSource = new StreamSource(schemaInputStream);
Schema schema = factory.newSchema(schemaSource);
Validator validator = schema.newValidator();
// DOMResult gives some DOMException...
validator.validate(new DOMSource(qualifyingPropertiesNode));
StreamSource streamSource = new StreamSource(tmpFile.toURI().toString());
ByteArrayOutputStream resultOutputStream = new ByteArrayOutputStream();
StreamResult streamResult = new StreamResult(resultOutputStream);
// validator.validate(streamSource, streamResult);
LOG.debug("result: " + resultOutputStream);
}
@Test
public void testSignEnvelopingDocumentOffice2010() throws Exception {
// setup
EnvelopedSignatureFacet envelopedSignatureFacet = new EnvelopedSignatureFacet();
KeyInfoSignatureFacet keyInfoSignatureFacet = new KeyInfoSignatureFacet(true, false, false);
SignaturePolicyService signaturePolicyService = new ExplicitSignaturePolicyService("urn:test",
"hello world".getBytes(), "description", "http://here.com");
XAdESSignatureFacet xadesSignatureFacet = new XAdESSignatureFacet(signaturePolicyService);
TimeStampService mockTimeStampService = EasyMock.createMock(TimeStampService.class);
RevocationDataService mockRevocationDataService = EasyMock.createMock(RevocationDataService.class);
XAdESXLSignatureFacet xadesXLSignatureFacet = new XAdESXLSignatureFacet(mockTimeStampService,
mockRevocationDataService);
XmlSignatureTestService testedInstance = new XmlSignatureTestService(envelopedSignatureFacet,
keyInfoSignatureFacet, xadesSignatureFacet, new Office2010SignatureFacet(), xadesXLSignatureFacet);
KeyPair keyPair = PkiTestUtils.generateKeyPair();
DateTime notBefore = new DateTime();
DateTime notAfter = notBefore.plusYears(1);
X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), "CN=Test", notBefore,
notAfter, null, keyPair.getPrivate(), true, 0, null, null, new KeyUsage(KeyUsage.nonRepudiation));
List<X509Certificate> certificateChain = new LinkedList<X509Certificate>();
/*
* We need at least 2 certificates for the XAdES-C complete certificate
* refs construction.
*/
certificateChain.add(certificate);
certificateChain.add(certificate);
RevocationData revocationData = new RevocationData();
final X509CRL crl = PkiTestUtils.generateCrl(certificate, keyPair.getPrivate());
revocationData.addCRL(crl);
OCSPResp ocspResp = PkiTestUtils.createOcspResp(certificate, false, certificate, certificate,
keyPair.getPrivate(), "SHA1withRSA");
revocationData.addOCSP(ocspResp.getEncoded());
// expectations
EasyMock.expect(mockTimeStampService.timeStamp(EasyMock.anyObject(byte[].class),
EasyMock.anyObject(RevocationData.class))).andStubAnswer(new IAnswer<byte[]>() {
public byte[] answer() throws Throwable {
Object[] arguments = EasyMock.getCurrentArguments();
RevocationData revocationData = (RevocationData) arguments[1];
revocationData.addCRL(crl);
return "time-stamp-token".getBytes();
}
});
EasyMock.expect(mockRevocationDataService.getRevocationData(EasyMock.eq(certificateChain)))
.andStubReturn(revocationData);
// prepare
EasyMock.replay(mockTimeStampService, mockRevocationDataService);
// operate
DigestInfo digestInfo = testedInstance.preSign(null, certificateChain, null, null, null);
// verify
assertNotNull(digestInfo);
assertEquals("SHA-1", digestInfo.digestAlgo);
assertNotNull(digestInfo.digestValue);
TemporaryTestDataStorage temporaryDataStorage = (TemporaryTestDataStorage) testedInstance
.getTemporaryDataStorage();
assertNotNull(temporaryDataStorage);
InputStream tempInputStream = temporaryDataStorage.getTempInputStream();
assertNotNull(tempInputStream);
Document tmpDocument = PkiTestUtils.loadDocument(tempInputStream);
LOG.debug("tmp document: " + PkiTestUtils.toString(tmpDocument));
Element nsElement = tmpDocument.createElement("ns");
nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS);
nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:xades", "http://uri.etsi.org/01903/v1.3.2#");
Node digestValueNode = XPathAPI.selectSingleNode(tmpDocument, "//ds:DigestValue", nsElement);
assertNotNull(digestValueNode);
String digestValueTextContent = digestValueNode.getTextContent();
LOG.debug("digest value text content: " + digestValueTextContent);
assertFalse(digestValueTextContent.isEmpty());
/*
* Sign the received XML signature digest value.
*/
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue);
byte[] signatureValue = cipher.doFinal(digestInfoValue);
/*
* Operate: postSign
*/
testedInstance.postSign(signatureValue, certificateChain);
// verify
EasyMock.verify(mockTimeStampService, mockRevocationDataService);
byte[] signedDocumentData = testedInstance.getSignedDocumentData();
assertNotNull(signedDocumentData);
Document signedDocument = PkiTestUtils.loadDocument(new ByteArrayInputStream(signedDocumentData));
LOG.debug("signed document: " + PkiTestUtils.toString(signedDocument));
NodeList signatureNodeList = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
assertEquals(1, signatureNodeList.getLength());
Element signatureNode = (Element) signatureNodeList.item(0);
// work-around for Java 7
Element signedPropertiesElement = (Element) signatureNode
.getElementsByTagNameNS(XAdESXLSignatureFacet.XADES_NAMESPACE, "SignedProperties").item(0);
signedPropertiesElement.setIdAttribute("Id", true);
DOMValidateContext domValidateContext = new DOMValidateContext(
KeySelector.singletonKeySelector(keyPair.getPublic()), signatureNode);
XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance();
XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext);
boolean validity = xmlSignature.validate(domValidateContext);
assertTrue(validity);
File tmpFile = File.createTempFile("xades-bes-", ".xml");
FileUtils.writeStringToFile(tmpFile, PkiTestUtils.toString(signedDocument));
LOG.debug("tmp file: " + tmpFile.getAbsolutePath());
Node resultNode = XPathAPI.selectSingleNode(signedDocument,
"ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties/xades:SignedSignatureProperties/xades:SigningCertificate/xades:Cert/xades:CertDigest/ds:DigestValue",
nsElement);
assertNotNull(resultNode);
// also test whether the XAdES extension is in line with the XAdES XML
// Schema.
// stax-api 1.0.1 prevents us from using
// "XMLConstants.W3C_XML_SCHEMA_NS_URI"
Node qualifyingPropertiesNode = XPathAPI.selectSingleNode(signedDocument,
"ds:Signature/ds:Object/xades:QualifyingProperties", nsElement);
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
LSResourceResolver xadesResourceResolver = new XAdESLSResourceResolver();
factory.setResourceResolver(xadesResourceResolver);
InputStream schemaInputStream = XAdESSignatureFacetTest.class.getResourceAsStream("/XAdESv141.xsd");
Source schemaSource = new StreamSource(schemaInputStream);
Schema schema = factory.newSchema(schemaSource);
Validator validator = schema.newValidator();
// DOMResult gives some DOMException...
validator.validate(new DOMSource(qualifyingPropertiesNode));
StreamSource streamSource = new StreamSource(tmpFile.toURI().toString());
ByteArrayOutputStream resultOutputStream = new ByteArrayOutputStream();
StreamResult streamResult = new StreamResult(resultOutputStream);
// validator.validate(streamSource, streamResult);
LOG.debug("result: " + resultOutputStream);
}
private static class XAdESLSResourceResolver implements LSResourceResolver {
private static final Log LOG = LogFactory.getLog(XAdESLSResourceResolver.class);
public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId,
String baseURI) {
LOG.debug("resolve resource");
LOG.debug("type: " + type);
LOG.debug("namespace URI: " + namespaceURI);
LOG.debug("publicId: " + publicId);
LOG.debug("systemId: " + systemId);
LOG.debug("base URI: " + baseURI);
if ("http://uri.etsi.org/01903/v1.3.2#".equals(namespaceURI)) {
return new LocalLSInput(publicId, systemId, baseURI, "/XAdES.xsd");
}
if ("http://www.w3.org/2000/09/xmldsig#".equals(namespaceURI)) {
return new LocalLSInput(publicId, systemId, baseURI, "/xmldsig-core-schema.xsd");
}
throw new RuntimeException("unsupported resource: " + namespaceURI);
}
}
private static class LocalLSInput implements LSInput {
private String publicId;
private String systemId;
private String baseURI;
private final String schemaResourceName;
public LocalLSInput(String publicId, String systemId, String baseURI, String schemaResourceName) {
this.publicId = publicId;
this.systemId = systemId;
this.baseURI = baseURI;
this.schemaResourceName = schemaResourceName;
}
public String getBaseURI() {
return this.baseURI;
}
public InputStream getByteStream() {
InputStream inputStream = XAdESSignatureFacetTest.class.getResourceAsStream(this.schemaResourceName);
return inputStream;
}
public boolean getCertifiedText() {
return true;
}
public Reader getCharacterStream() {
InputStream inputStream = getByteStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
return reader;
}
public String getEncoding() {
return "UTF-8";
}
public String getPublicId() {
return this.publicId;
}
public String getStringData() {
InputStream inputStream = getByteStream();
String stringData;
try {
stringData = IOUtils.toString(inputStream);
} catch (IOException e) {
throw new RuntimeException("I/O error: " + e.getMessage(), e);
}
return stringData;
}
public String getSystemId() {
return this.systemId;
}
public void setBaseURI(String baseURI) {
this.baseURI = baseURI;
}
public void setByteStream(InputStream byteStream) {
throw new UnsupportedOperationException();
}
public void setCertifiedText(boolean certifiedText) {
throw new UnsupportedOperationException();
}
public void setCharacterStream(Reader characterStream) {
throw new UnsupportedOperationException();
}
public void setEncoding(String encoding) {
throw new UnsupportedOperationException();
}
public void setPublicId(String publicId) {
this.publicId = publicId;
}
public void setStringData(String stringData) {
throw new UnsupportedOperationException();
}
public void setSystemId(String systemId) {
this.systemId = systemId;
}
}
}